#!/usr/bin/python

#    This file is part of CamVue.
#
#    CamVue is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    CamVue is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with CamVue.  If not, see <http://www.gnu.org/licenses/>.
#
#    Copyright 2012  Jeremy Mortis (mortis@tansay.ca)

import Tkinter
import Tkconstants 
import tkMessageBox
import Image
import ImageTk
import time
import sys
import logging
import ConfigParser
import re
import webbrowser
import os

if sys.platform == 'win32':
    import cameraWindows as Camera
else:
    import cameraLinux as Camera
    
class GUI(Tkinter.Tk):

    def __init__(self):

        Tkinter.Tk.__init__(self)

        if sys.platform == 'win32':
            self.wm_iconbitmap("camvue.ico")

        logging.info("Getting device list")
        self.deviceList = Camera.getDeviceList()
        self.device = None

        self.protocol("WM_DELETE_WINDOW", self.delete_window)
        self.currentImage = None
        self.currentPhotoImage = None  # must be persistent
        self.resetImageSize = False
        self.afterId = None
        self.fullScreen = False

        self.varDevice = Tkinter.StringVar()
        self.varLockImageSize = Tkinter.BooleanVar()

        self.varAspect = Tkinter.StringVar()
        self.varAspect.trace('w', self.change_varAspect)

        logging.info("Getting configuration")
        self.get_config()

        self.varDevice.trace('w', self.change_varDevice)

        self.refreshTimes = [0] * 32
        self.processingTimes = [0] * 32
        self.imageCount = 0
        self.lastRefreshTime = time.time()

        self.statusBar = Tkinter.Label(self, text="", bd=1, relief=Tkinter.SUNKEN, anchor=Tkinter.E)

        self.imageFrame = Tkinter.Frame(self)
        self.imageFrame.pack(fill=Tkinter.BOTH, expand=1)

        self.imageLabel = Tkinter.Label(self.imageFrame, text="\n\n\n\n\nInitializing...")
        self.imageLabel.bind("<Button-1>", self.restore_menu_and_status_bar)
        self.imageLabel.pack()

        if self.showMenuBar == 1:
            self.show_menu()
            self.show_status_bar()

        logging.info("Scheduling first image retrieval")
        self.afterId = self.imageLabel.after(1000, self.update_photo)

        logging.info("GUI started")

    def get_config(self):

        self.configDefaults = {"title": "CamVue", 
                               "geometry": "640x510+50+50", 
                               "imageWidth": 640, 
                               "imageHeight": 480, 
                               "showmenubar": 1, 
                               "fixedAspectRatio": 1.3333, 
                               "aspect": "4:3", 
                               "lockImageSize": 0,
                               "device": "",
                               }
        self.config = ConfigParser.RawConfigParser(self.configDefaults)

        if sys.platform == 'win32':
            self.configFilename = os.path.join(os.environ['APPDATA'], "camvue", "camvue.ini")
        else:
            self.configFilename = os.path.expanduser(os.path.join("~", ".camvue"))

        f = self.config.read(self.configFilename)

        self.title(self.config.get("DEFAULT", "title"))
        self.geometry(self.config.get("DEFAULT", "geometry"))
        self.showMenuBar = self.config.getint("DEFAULT", "showmenubar")
        self.imageWidth = self.config.getint("DEFAULT", "imageWidth")
        self.imageHeight = self.config.getint("DEFAULT", "imageHeight")
        self.fixedAspectRatio = self.config.getfloat("DEFAULT", "fixedAspectRatio")
        self.varAspect.set(self.config.get("DEFAULT", "aspect"))
        self.varLockImageSize.set(self.config.getint("DEFAULT", "lockImageSize"))

        deviceName = self.config.get("DEFAULT", "device")
        self.varDevice.set("")
        if deviceName == "":
            if len(self.deviceList) > 0:
                self.varDevice.set(self.deviceList[0][0])
                logging.info("Using default device: %s" % self.varDevice.get())
        else:
            for deviceEntry in self.deviceList:
                if deviceName == str(deviceEntry[0]):
                    self.varDevice.set(deviceName)
                    logging.info("Found previous device: %s" % self.varDevice.get())
        if self.varDevice.get() == "":
            logging.info("Previous device no longer valid: %s" % deviceName)
                
    def restore_menu_and_status_bar(self, *args):

        if self.fullScreen:
            self.update_idletasks()
            self.overrideredirect(False)
            self.geometry(self.prevGeometry)
            self.fullScreen = False
            self.update()

        if self.showMenuBar == 0:
            self.show_menu()
            self.show_status_bar()
            
    def show_menu(self, *args):

        self.showMenuBar = 1

        self.menu = Tkinter.Menu(self)

        self.menuFile = Tkinter.Menu(self.menu, tearoff=0)
        self.menu.add_cascade(label="File", menu=self.menuFile, underline=0)
        self.menuFile.add_command(label="Quit", command=self.delete_window, underline=0)

        self.menuView = Tkinter.Menu(self.menu, tearoff=0)
        self.menu.add_cascade(label="View", menu=self.menuView, underline=0)

        self.menuDevice = Tkinter.Menu(self.menuView, tearoff=0)
        self.menuView.add_cascade(label="Device", menu=self.menuDevice, underline=0)
        if len(self.deviceList) == 0:
            self.menuDevice.add_radiobutton(label="No devices available", state=Tkconstants.DISABLED)
        else:
            for device in self.deviceList:
                label = "%s (%s)" % device
                self.menuDevice.add_radiobutton(label=label, variable=self.varDevice, value=device[0])

        self.menuAspect = Tkinter.Menu(self.menuView, tearoff=0)
        self.menuView.add_cascade(label="Aspect", menu=self.menuAspect, underline=0)
        self.menuAspect.add_radiobutton(label="Flexible", variable=self.varAspect, value="Flexible", underline=0)
        self.menuAspect.add_radiobutton(label="4:3 (1.33)", variable=self.varAspect, value="4:3", underline=0)
        self.menuAspect.add_radiobutton(label="3:2 (1.50)", variable=self.varAspect, value="3:2", underline=0)
        self.menuAspect.add_radiobutton(label="16:9 (1.78)", variable=self.varAspect, value="16:9", underline=0)
        self.menuAspect.add_radiobutton(label="Set current", variable=self.varAspect, value="Set", underline=0)

        self.menuView.add_checkbutton(label="Lock image size", variable=self.varLockImageSize, underline=0)
        self.menuView.add_separator()
        self.menuView.add_command(label="Hide Menubar", command=self.hide_menu, underline=0)
        self.menuView.add_command(label="Full screen", command=self.set_screen_full, underline=0)

        self.menuHelp = Tkinter.Menu(self.menu, tearoff=0)
        self.menu.add_cascade(label="Help", menu=self.menuHelp, underline=0)
        self.menuHelp.add_command(label="Wiki", command=self.show_wiki, underline=0)
        self.menuHelp.add_command(label="About", command=self.show_infobox, underline=0)

        self.configure(menu=self.menu)

    def hide_menu(self, *args):

        self.showMenuBar = 0
        self.menu.destroy()

        self.statusBar.pack_forget()

        geometry = self.parse_geometry(self.geometry())
        geometry[1] -= self.statusBar.winfo_height()
        self.geometry("%ix%i+%i+%i" % tuple(geometry))

    def set_screen_full(self, *args):

        if self.showMenuBar == 1:
            self.hide_menu()

        self.update_idletasks()
        w, h = self.winfo_screenwidth(), self.winfo_screenheight()
        self.overrideredirect(True)
        self.prevGeometry = self.geometry()
        self.geometry("%ix%i+0+0" % (w, h))
        self.fullScreen = True

    def change_varAspect(self, var, index, mode):

        if self.varAspect.get() == "Flexible":
            self.fixedAspectRatio = 1.0 
        elif self.varAspect.get() == "4:3":
            self.fixedAspectRatio = float(4) / 3 
        elif self.varAspect.get() == "3:2":
            self.fixedAspectRatio = float(3) / 2 
        elif self.varAspect.get() == "16:9":
            self.fixedAspectRatio = float(16) / 9
        elif self.varAspect.get() == "Set":
            self.fixedAspectRatio = float(self.currentPhotoImage.width()) / self.currentPhotoImage.height()

    def change_varDevice(self, *args):

        if self.afterId:
            self.imageLabel.after_cancel(self.afterId)
            self.afterId = None

        if self.device:
            self.device.close()
            self.device = None

        self.afterId = self.imageLabel.after(100, self.update_photo)

    def show_status_bar(self):

        self.imageFrame.pack_forget()
        self.statusBar.pack(side=Tkinter.BOTTOM, fill=Tkinter.X)
        self.imageFrame.pack(fill=Tkinter.BOTH, expand=1)

    def update_photo(self):

        if not self.device:

            if self.varDevice.get() == "":
                tkMessageBox.showerror("Error", "Please select a device")
                return

            try:
                logging.info("Opening device: %s" % self.varDevice.get())
                self.device = Camera.Camera(self.varDevice.get())    
            except Exception, e:
                tkMessageBox.showerror("Error", "Unable to connect to %s: %s " % (self.varDevice.get(), str(e)))
                return
    
        startProcessingTime = time.time()

        try:
            self.currentImage = self.device.getImage()
        except Exception, e:
            tkMessageBox.showerror("Error", "Camera disconnected")
            logging.info("Camera disconnected: %s" % str(e))
            self.device = None
            return

        if self.varLockImageSize.get():
            newWidth = self.imageWidth
            newHeight = self.imageHeight
        elif self.varAspect.get() == "Flexible":
            newWidth = self.imageFrame.winfo_width()
            newHeight = self.imageFrame.winfo_height()
        else:
            newHeight = self.imageFrame.winfo_height()
            newWidth = int(newHeight * self.fixedAspectRatio)

        newWidth = max(newWidth, 50)
        newHeight = max(newHeight, 50)

        self.currentPhotoImage = ImageTk.PhotoImage(self.currentImage.resize((newWidth, newHeight)))
        self.imageLabel.configure(image = self.currentPhotoImage)
        self.imageWidth = newWidth
        self.imageHeight = newHeight

        newAspectRatio = float(newWidth) / float(newHeight)

        if self.showMenuBar:
            self.imageCount += 1
            now = time.time()
            processingTime = now - startProcessingTime
            refreshTime = now - self.lastRefreshTime
            self.lastRefreshTime = now
            n = len(self.processingTimes)
            self.processingTimes[self.imageCount % n] = processingTime
            self.refreshTimes[self.imageCount % n] = refreshTime
            frameRate = n / float(sum(self.refreshTimes)) 
            avgProcessingTime = float(sum(self.processingTimes)) / n

            self.statusBar.configure(text="size: %ix%i  aspect: %.2f  fps: %.1f cpu: %.3f" % (self.imageWidth, self.imageHeight, newAspectRatio, frameRate, avgProcessingTime))

        self.afterId = self.imageLabel.after(10, self.update_photo)

    def show_infobox(self):
        aspectRatio = float(self.imageLabel.winfo_width())/self.imageLabel.winfo_height()
        tkMessageBox.showinfo("", "CamVue 1.0\n\n" 
                              + "Copyright 2012 by Jeremy Mortis (mortis@tansay.ca)\n\n"
                              + "This program is free software made available under the GPLv3 license.")

    def show_wiki(self):
        webbrowser.open("https://code.google.com/p/camvue")

    def delete_window(self):

        self.quit()
        self.save_config()

    def save_config(self):

        self.config.set("DEFAULT", "title", self.title())
        self.config.set("DEFAULT", "geometry", self.geometry())
        self.config.set("DEFAULT", "showmenubar", self.showMenuBar)
        self.config.set("DEFAULT", "fixedAspectRatio", self.fixedAspectRatio)
        self.config.set("DEFAULT", "aspect", self.varAspect.get())
        self.config.set("DEFAULT", "device", self.varDevice.get())

        if self.currentPhotoImage:
            self.config.set("DEFAULT", "imageWidth", self.currentPhotoImage.width())
            self.config.set("DEFAULT", "imageHeight", self.currentPhotoImage.height())

        if self.varLockImageSize.get():
            self.config.set("DEFAULT", "lockImageSize", 1)
        else:
            self.config.set("DEFAULT", "lockImageSize", 0)

        with open(self.configFilename, 'wb') as configfile:
            self.config.write(configfile)        

    def parse_geometry(self, s):
        m = re.match("(\d+)x(\d+)([-+]\d+)([-+]\d+)", s)
        if not m:
            raise ValueError("failed to parse geometry string: %s" % s)
        return map(int, m.groups())

if __name__ == '__main__':

    if sys.platform == 'win32':
        logFilename = os.path.join(os.environ['APPDATA'], "camvue", "camvue.log")
    else:
        logFilename = os.path.expanduser(os.path.join("~", "camvue.log"))

    logging.basicConfig(filename=logFilename, format='%(asctime)s %(message)s', level=logging.INFO)
    logging.info("Application started")

    gui = GUI()

    gui.mainloop()

    gui.destroy()

    logging.info("GUI terminated")


